﻿using System;
using System.Collections.Generic;
using System.Text;
using log4net;
using UPNP.IO;

namespace RouterControl.Whs
{
    /// <summary>
    /// commonly wraps the internet gateway services and
    /// provides common functions which act accordingly to the
    /// available services
    /// </summary>
    public class CommonInternetGateway
    {
        #region structures, classes, etc

        public enum ConnectionStatusValues
        {
            Authenticating,
            Connected,
            Connecting,
            Disconnected,
            Disconnecting,
            PendingDisconnect,
            Unconfigured,
        }

        public struct SummaryData
        {
            // general information
            public UInt32? UptimeSeconds;
            public UInt32? UploadMaxBitRate;
            public UInt32? DownloadMaxBitRate;
            public ConnectionStatusValues? ConnectionStatus;

            // traffic data
            public UInt32? UploadRateBytesPerSecond;
            public UInt32? DownloadRateBytesPerSecond;
        }

        public struct PortForwarding
        {
            public string Id            // for internal identification of entries
            {
                get
                {
                    return DeviceHelpers.CreateId(
                        this.RemoteHost,
                        this.ExternalPort.ToString(),
                        this.InternalClient,
                        this.IsUDP.ToString()
                        );
                }
            }

            public bool Enabled;

            public string RemoteHost;            // empty string means wildcard; DEFAULT ON MOST ROUTERS!
            public ushort ExternalPort;          // 0 means all ports are forwarded

            public string InternalClient;        // IP or DNS name; DNS name might not be supported by all routers
            public ushort InternalPort;

            public bool IsUDP;                   // is UDP, not TCP
            public string PortMappingDescription;
            public uint LeaseDurationSeconds;    // 0 means infinite; non-infinite might not be supported on all routers

            public override int GetHashCode()
            {
                long hash = (this.RemoteHost ?? string.Empty).GetHashCode() + 
                            357 * this.ExternalPort +
                            8935 * (this.InternalClient ?? string.Empty).GetHashCode() +
                            98344 * this.InternalPort +
                            783523 * (this.PortMappingDescription ?? string.Empty).GetHashCode() +
                            5612347 * this.LeaseDurationSeconds +
                            87345687 * (this.IsUDP ? 1 : 0);

                if (!this.Enabled)
                    hash = ~hash;

                return (int)hash;
            }

            public override string ToString()
            {
                var sb = new StringBuilder();

                sb.Append(string.IsNullOrEmpty(this.RemoteHost) ? "*" : this.RemoteHost);
                sb.Append(" => ");
                sb.Append(this.ExternalPort);
                sb.Append(" (");
                sb.Append(this.IsUDP ? "UDP" : "TCP");
                sb.Append(") => ");
                sb.Append(this.InternalClient);
                sb.Append(':');
                sb.Append(this.InternalPort);
                sb.Append(" (");
                sb.Append(this.Enabled ? "ON" : "OFF");
                sb.Append(") # ");
                sb.Append(this.PortMappingDescription);

                return sb.ToString();
            }

            public PortForwarding(string remoteHost, ushort externalPort, string internalClient, ushort internalPort, bool isUDP, uint durationSeconds, string description)
            {
                if (string.IsNullOrEmpty(remoteHost))
                    throw new ArgumentException("null or empty", "remoteHost");

                if (string.IsNullOrEmpty(internalClient))
                    throw new ArgumentException("null or empty", "internalClient");

                if (internalPort <= 0)
                    throw new ArgumentOutOfRangeException("wildcard value 0 is not allowed", "internalPort");

                this.Enabled = true;
                this.RemoteHost = remoteHost;
                this.ExternalPort = externalPort;
                this.InternalClient = internalClient;
                this.InternalPort = internalPort;
                this.IsUDP = isUDP;
                this.PortMappingDescription = description;
                this.LeaseDurationSeconds = durationSeconds;
            }

            public PortForwarding(ushort externalPort, string internalClient, ushort internalPort, bool isUDP, uint durationSeconds, string description)
            {
                if (string.IsNullOrEmpty(internalClient))
                    throw new ArgumentException("null or empty", "internalClient");

                if (internalPort <= 0)
                    throw new ArgumentOutOfRangeException("wildcard value 0 is not allowed", "internalPort");

                this.Enabled = true;
                this.RemoteHost = string.Empty;
                this.ExternalPort = externalPort;
                this.InternalClient = internalClient;
                this.InternalPort = internalPort;
                this.IsUDP = isUDP;
                this.PortMappingDescription = description;
                this.LeaseDurationSeconds = durationSeconds;
            }

            public PortForwarding(ushort externalPort, string internalClient, ushort internalPort, bool isUDP, string description)
            {
                if (string.IsNullOrEmpty(internalClient))
                    throw new ArgumentException("null or empty", "internalClient");

                if (internalPort <= 0)
                    throw new ArgumentOutOfRangeException("wildcard value 0 is not allowed", "internalPort");

                this.Enabled = true;
                this.RemoteHost = string.Empty;
                this.ExternalPort = externalPort;
                this.InternalClient = internalClient;
                this.InternalPort = internalPort;
                this.IsUDP = isUDP;
                this.PortMappingDescription = description;
                this.LeaseDurationSeconds = 0;
            }

            internal PortForwarding(dynamic e)
            {
                this.Enabled = e.NewEnabled;
                this.RemoteHost = e.NewRemoteHost;
                this.ExternalPort = e.NewExternalPort;
                this.InternalPort = e.NewInternalPort;
                this.IsUDP = (e.NewProtocol == "UDP");
                this.InternalClient = e.NewInternalClient;
                this.PortMappingDescription = e.NewPortMappingDescription;
                this.LeaseDurationSeconds = e.NewLeaseDuration;
            }
        }

        private class CommonServiceAction
        {
            public delegate dynamic CommonServiceActionDelegate();

            private static readonly ILog _log = LogManager.GetLogger(typeof(CommonInternetGateway));   // do not use specific logger
            private CommonServiceActionDelegate _action;
            private dynamic _value;
            private bool _tried = false;

            public dynamic ActionReturn
            {
                get
                {
                    // already tried?
                    if (_tried)
                        return _value;

                    // retrieve
                    try
                    {
                        _value = _action();
                    }
                    catch (NotImplementedException)
                    {
                        // ignore
                    }
                    catch (Exception e)
                    {
                        _log.Error("Failed to load data from router", e);

                        throw;
                    }
                    finally
                    {
                        _tried = true;
                    }

                    return _value;
                }
            }

            public void Reset()
            {
                _tried = false;
            }

            public CommonServiceAction(CommonServiceActionDelegate action)
            {
                if (action == null)
                    throw new ArgumentNullException("action");

                _action = action;
            }
        }

        #endregion

        private readonly ILog _log = LogManager.GetLogger(typeof(CommonInternetGateway));
        private RootDevice _device;
        private object _updateDataLock = new object();
        private IList<DeviceService> _services;
        private dynamic _config;
        private dynamic _conn;

        public string RootDeviceId
        {
            get { return _device.UniqueDeviceName; }
        }

        public RootDevice Device
        {
            get { return _device; }
        }

        private readonly CommonServiceAction _x_GetICSStatistics;
        private readonly CommonServiceAction _getAddonInfos;        // used by AVM
        private readonly CommonServiceAction _getCommonLinkProperties;
        private readonly CommonServiceAction _getTotalBytesSent;
        private readonly CommonServiceAction _getTotalBytesReceived;
        private readonly CommonServiceAction _getStatusInfo;
        private readonly CommonServiceAction _getLinkLayerMaxBitRates;

        public void ClearCacheAll()
        {
            _x_GetICSStatistics.Reset();
            _getAddonInfos.Reset();
            _getCommonLinkProperties.Reset();
            _getTotalBytesSent.Reset();
            _getTotalBytesReceived.Reset();
            _getStatusInfo.Reset();
            _getLinkLayerMaxBitRates.Reset();
        }

        public void ClearCacheTrafficData()
        {
            _x_GetICSStatistics.Reset();
            _getAddonInfos.Reset();
            _getTotalBytesSent.Reset();
            _getTotalBytesReceived.Reset();
        }

        public uint? UploadRateBytesPerSecond
        {
            get
            {
                uint val = 0;

                if (_x_GetICSStatistics.ActionReturn != null)
                    val = _x_GetICSStatistics.ActionReturn.Value.TotalBytesSent;

                else if (_getAddonInfos.ActionReturn != null)
                    val = _getAddonInfos.ActionReturn.Value.NewByteSendRate;

                else if (_getTotalBytesSent.ActionReturn != null)
                    val = _getTotalBytesSent.ActionReturn.Value;

                //if (val == 0)
                //    return null;

                return val;
            }
        }

        public uint? DownloadRateBytesPerSecond
        {
            get
            {
                uint val = 0;

                if (_x_GetICSStatistics.ActionReturn != null)
                    val = _x_GetICSStatistics.ActionReturn.Value.TotalBytesReceived;

                else if (_getAddonInfos.ActionReturn != null)
                    val = _getAddonInfos.ActionReturn.Value.NewByteReceiveRate;

                else if (_getTotalBytesReceived.ActionReturn != null)
                    val = _getTotalBytesReceived.ActionReturn.Value;

                //if (val == 0)
                //    return null;

                return val;
            }
        }

        public uint? UptimeSeconds
        {
            get
            {
                uint val = 0;

                if (_x_GetICSStatistics.ActionReturn != null)
                    val = _x_GetICSStatistics.ActionReturn.Value.Uptime;

                else if (_getStatusInfo.ActionReturn != null)
                    val = _getStatusInfo.ActionReturn.Value.NewUptime;

                if (val == 0)
                    return null;

                return val;
            }
        }

        public uint? UploadMaxBitRate
        {
            get
            {
                uint val = 0;

                if (_getCommonLinkProperties.ActionReturn != null)
                    val = _getCommonLinkProperties.ActionReturn.Value.NewLayer1UpstreamMaxBitRate;

                else if (_getLinkLayerMaxBitRates.ActionReturn != null)
                    val = _getLinkLayerMaxBitRates.ActionReturn.Value.NewUpstreamMaxBitRate;

                if (val == 0)
                    return null;

                return val;
            }
        }

        public uint? DownloadMaxBitRate
        {
            get
            {
                uint val = 0;

                if (_x_GetICSStatistics.ActionReturn != null)
                    val = _x_GetICSStatistics.ActionReturn.Value.Layer1DownstreamMaxBitRate;

                else if (_getCommonLinkProperties.ActionReturn != null)
                    val = _getCommonLinkProperties.ActionReturn.Value.NewLayer1DownstreamMaxBitRate;

                else if (_getLinkLayerMaxBitRates.ActionReturn != null)
                    val = _getLinkLayerMaxBitRates.ActionReturn.Value.NewDownstreamMaxBitRate;

                if (val == 0)
                    return null;

                return val;
            }
        }

        public ConnectionStatusValues? ConnectionStatus
        {
            get
            {
                if (_getStatusInfo.ActionReturn != null)
                    return ParseConnectionStatus(_getStatusInfo.ActionReturn.NewConnectionStatus);

                return null;
            }
        }

        public SummaryData Summary
        {
            get
            {
                return new SummaryData()
                       {
                           DownloadRateBytesPerSecond = this.DownloadRateBytesPerSecond,
                           UploadRateBytesPerSecond = this.UploadRateBytesPerSecond,
                           DownloadMaxBitRate = this.DownloadMaxBitRate,
                           UploadMaxBitRate = this.UploadMaxBitRate,
                           UptimeSeconds = this.UptimeSeconds,
                           ConnectionStatus = this.ConnectionStatus
                       };
            }
        }

        public IList<PortForwarding> GetPortForwardings()
        {
            var v = new List<PortForwarding>();

            if (_conn != null)
            {
                var entryCount = _conn.PortMappingNumberOfEntries;

                if (entryCount > 0)
                {
                    for (ushort i = 0; i < entryCount; i++)
                    {
                        v.Add(new PortForwarding(_conn.GetGenericPortMappingEntry(i)));
                    }
                }
            }
            else
            {
                return null;    // not supported
            }

            return v.AsReadOnly();
        }

        public void AddOrUpdatePortForwarding(PortForwarding f)
        {
            if (_conn != null)
            {
                _conn.AddPortMapping(
                    f.RemoteHost,
                    f.ExternalPort,
                    (f.IsUDP ? "UDP" : "TCP"),
                    f.InternalPort,
                    f.InternalClient,
                    f.Enabled,
                    f.PortMappingDescription,
                    f.LeaseDurationSeconds
                    );
            }
            else
            {
                throw new NotImplementedException(
                    "device does not contain a service provided port forwarding: " + _device.UniqueDeviceName + 
                    " (" + _device.Type + ")"
                    );
            }
        }

        public void DeletePortForwarding(PortForwarding f)
        {
            if (_conn != null)
            {
                _conn.DeletePortMapping(
                    f.RemoteHost,
                    f.ExternalPort,
                    (f.IsUDP ? "UDP" : "TCP")
                    );
            }
            else
            {
                throw new NotImplementedException(
                    "device does not contain a service provided port forwarding: " + _device.UniqueDeviceName +
                    " (" + _device.Type + ")"
                    );
            }
        }

        #region helper methods

        public static CommonInternetGateway.ConnectionStatusValues? ParseConnectionStatus(DynamicEnumerable s)
        {
            switch (s.Value)
            {
                case "Authenticating":
                    return CommonInternetGateway.ConnectionStatusValues.Authenticating;

                case "Connected":
                    return CommonInternetGateway.ConnectionStatusValues.Connected;

                case "Connecting":
                    return CommonInternetGateway.ConnectionStatusValues.Connecting;

                case "Disconnected":
                    return CommonInternetGateway.ConnectionStatusValues.Disconnected;

                case "Disconnecting":
                    return CommonInternetGateway.ConnectionStatusValues.Disconnecting;

                case "PendingDisconnect":
                    return CommonInternetGateway.ConnectionStatusValues.PendingDisconnect;

                case "Unconfigured":
                    return CommonInternetGateway.ConnectionStatusValues.Unconfigured;

                default:
                    throw new ArgumentException("unknown status: " + s.ToString(), "s");
            }
        }

        #endregion

        public CommonInternetGateway(RootDevice gwDevice)
        {
            if (gwDevice == null)
                throw new ArgumentNullException("gwDevice");

            if (gwDevice.Type != RootDeviceTypeIds.InternetGatewayDeviceType)
                throw new ArgumentException("not an internet gateway device", "gwDevice");

            _device = gwDevice;
            _services = _device.GetServicesRecursive();

            // get services
            foreach (var s in _services)
            {
                switch (s.ServiceTypeIdentifier)
                {
                    case "urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1":
                    case "urn:dslforum-org:service:WANCommonInterfaceConfig:1":
                        _config = (dynamic)s;

                        break;

                    case "urn:schemas-upnp-org:service:WANIPConnection:1":
                    case "urn:schemas-upnp-org:service:WANPPPConnection:1":
                    case "urn:dslforum-org:service:WANIPConnection:1":
                    case "urn:dslforum-org:service:WANPPPConnection:1":
                        _conn = (dynamic)s;

                        break;
                }
            }

            // create delegates
            _x_GetICSStatistics = new CommonServiceAction(
                delegate()
                {
                    if (_config == null)
                        return null;

                    return _config.X_GetICSStatistics();
                });

            _getAddonInfos = new CommonServiceAction(
                delegate()
                {
                    if (_config == null)
                        return null;

                    return _config.GetAddonInfos();
                });

            _getCommonLinkProperties = new CommonServiceAction(
                delegate()
                {
                    if (_config == null)
                        return null;

                    return _config.GetCommonLinkProperties();
                });

            _getTotalBytesSent = new CommonServiceAction(
                delegate()
                {
                    if (_config == null)
                        return null;

                    return _config.GetTotalBytesSent();
                });

            _getTotalBytesReceived = new CommonServiceAction(
                delegate()
                {
                    if (_config == null)
                        return null;

                    return _config.GetTotalBytesReceived();
                });

            _getStatusInfo = new CommonServiceAction(
                delegate()
                {
                    if (_conn == null)
                        return null;

                    return _conn.GetStatusInfo();
                });

            _getLinkLayerMaxBitRates = new CommonServiceAction(
                delegate()
                {
                    if (_conn == null)
                        return null;

                    return _conn.GetLinkLayerMaxBitRates();
                });
        }
    }
}